package com.tengju.data.domain.shared.tree;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.tengju.data.domain.shared.IdObject;
import lombok.extern.slf4j.Slf4j;

import javax.validation.constraints.NotNull;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.stream.Collectors;

@Slf4j
public class Forest<T extends TreeData> {

    private static final ExecutorService forkJoinPool = new ForkJoinPool();


    private final Set<Tree<T>> trees = new HashSet<>();

    @JsonIgnore
    private final Map<IdObject, Tree<T>> allForestNodes = new ConcurrentHashMap<>();

    private final Set<IdObject> handlingNodes = new HashSet<>();

    private static final boolean DEBUG = false;

    private boolean stable = false;

    private void checkStable() {
        if (!stable) {
            throw new TreeException("Forest is under building, can't read now.");
        }
    }

    public Set<Tree<T>> getTrees() {
        checkStable();
        return trees;
    }

    public Set<StableTree<T>> getStableTrees() {
        checkStable();
        return trees.stream()
                .map( v -> new StableTree<>(v.getRoot()))
                .collect(Collectors.toSet());
    }

    public int countNodes() {
        checkStable();
        return allForestNodes.size();
    }

    public TreeNode<T> findNode(IdObject nodeId) {
        checkStable();
        return Optional.ofNullable(findTree(nodeId))
                .map(tree -> tree.findNode(nodeId))
                .orElse(null);
    }


    public Tree<T> findTree(IdObject nodeId) {
        checkStable();
        return allForestNodes.get(nodeId);
    }

    public TreeNode<T> findNodeById(IdObject id) {
        checkStable();
        return Optional.ofNullable(allForestNodes.get(id))
                .map(tree -> tree.findNodeById(id))
                .orElse(null);
    }


    private TreeNode<T> tryFindNode(IdObject nodeId) {
        return Optional.ofNullable(tryFindTree(nodeId))
                .map(tree -> tree.findNode(nodeId))
                .orElse(null);
    }


    private Tree<T> tryFindTree(IdObject nodeId) {
        return allForestNodes.get(nodeId);
    }


    public void finishSetUp() {
        synchronized (handlingNodes) {
            Set<Tree<T>> stableTrees = new HashSet<>();
            trees.forEach(tree -> stableTrees.add(new StableTree<>(tree.getRoot())));
            trees.clear();
            trees.addAll(stableTrees);
            stable = true;
        }
    }


    public Future<Tree<T>> asyncPutNode(@NotNull T node, IdObject parentId) {
        return forkJoinPool.submit(() -> putNode(node, parentId));
    }


    private Tree<T> putNode(@NotNull T nodeData, IdObject parentId) {
        Set<IdObject> todoNodes = null;
        TreeNode<T> node;
        TreeNode<T> parentNode;
        synchronized (handlingNodes) {
            try {
                while (handlingNodes.contains(nodeData.getId()) || handlingNodes.contains(parentId)) {
                    handlingNodes.wait();
                }
                debug(nodeData.getId(), parentId);
                todoNodes = handleNodes(nodeData.getId(), parentId);
                node = tryFindNode(nodeData.getId());
                parentNode = parentId == null ? null : tryFindNode(parentId);
                return putNode(nodeData, parentId, node, parentNode);
            } catch (Exception e) {
                log.info(String.format(System.currentTimeMillis() + " " +
                        Thread.currentThread().getId() + " error in %s %s： %s", nodeData, parentId, e.getMessage()));
                return null;
            } finally {
                if (todoNodes != null) {
                    handlingNodes.removeAll(todoNodes);
                    if (DEBUG) {
                        log.info(String.format(Thread.currentThread().getId() + " release node %s and parent %s", nodeData, parentId));
                    }
                }
                handlingNodes.notifyAll();
            }
        }
    }

    private Tree<T> putNode(@NotNull T nodeData, IdObject parentId, TreeNode<T> node, TreeNode<T> parentNode) {
        if (parentId == null) { //添加的是根节点
            if (node == null) { //种树
                return seed(nodeData);
            } else { //已存在, 刷新节点数据
                node.fillData(nodeData);
                return allForestNodes.get(node.getId());
            }
        } else { //添加的是子节点
            if (parentNode == null) { //父节点不存在，以父节点为根种一颗树
                TreeData parentData = new TreeData() {
                    @Override
                    public IdObject getId() {
                        return parentId;
                    }

                    @Override
                    public boolean isVirtual() {
                        return true;
                    }
                };
                Tree<T> newTree = seed((T) parentData);
                parentNode = newTree.getRoot();
            }
            boolean hasMerge = false;
            if (node == null) {
                node = new TreeNode<>(nodeData);
            } else {
                hasMerge = true;
                node.fillData(nodeData);
            }
            Tree<T> result = appendChild(parentNode, node);
            if (hasMerge) { //当前节被点合并到父节点了, 删除之前以当前节点为根的树
                trees.remove(new Tree<>(nodeData));
            }
            return result;
        }
    }


    private Set<IdObject> handleNodes(@NotNull IdObject node, IdObject parent) {
        Set<IdObject> todoNodes = new HashSet<>();
        todoNodes.add(node);
        Tree<T> tree = tryFindTree(node);
        if (tree != null) {
            todoNodes.add(tree.getRoot().getId());
        }

        if (parent != null) {
            todoNodes.add(parent);
        }
        handlingNodes.addAll(todoNodes);
        return todoNodes;
    }

    private Tree<T> seed(T root) {
        checkExist(root.getId());
        Tree<T> tree = new Tree<>(root);
        trees.add(tree);
        allForestNodes.put(root.getId(), tree);
        debug(root.getId(), null);
        return tree;
    }

    private void plant(Tree<T> tree) {
        tree.getAllTreeNodes().forEach((id, node) -> checkExist(id));
        trees.add(tree);
        tree.getAllTreeNodes().forEach((id, node) -> allForestNodes.put(id, tree));
    }

    private Tree<T> appendChild(@NotNull TreeNode<T> parent, @NotNull TreeNode<T> child) {
        Tree<T> tree = Optional.ofNullable(allForestNodes.get(parent.getData().getId()))
                .orElseGet(() -> {
                    Tree<T> newTree = new Tree<>(parent);
                    plant(newTree);
                    return newTree;
                });
        tree.addNodes(parent, Collections.singleton(child));
        allForestNodes.put(child.getId(), tree);
        return tree;
    }


    private void checkExist(IdObject id) {
        if (allForestNodes.containsKey(id)) {
            throw new TreeException(String.format("Node with data %s already exist in this forest", id));
        }
    }

    public void debug(IdObject node, IdObject parent) {
        if (DEBUG) {
            log.info(System.currentTimeMillis() + "  " +
                    Thread.currentThread().getId() +
                    "------------------" + node + "---" + parent + "------------------------------------debug\n" + trees.stream()
                    .map(tree -> String.format("tree(%s)：%n%s", tree.getRoot(), tree))
                    .collect(Collectors.joining("\n")));
        }
    }

    @Override
    public String toString() {
        return trees.stream()
                .map(tree -> String.format("=======tree(%s)=======%n%s", tree.getRoot(), tree.toString()))
                .collect(Collectors.joining("\n"));
    }

}
